/*
 * Zed Attack Proxy (ZAP) and its related class files.
 * 
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 * 
 * Copyright 2017 The ZAP Development Team
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.zaproxy.zap.utils;

import java.awt.event.ActionEvent;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JOptionPane;
import javax.swing.JTable;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.view.View;
import org.zaproxy.zap.view.widgets.WritableFileChooser;

/**
 * An {@code AbstractAction} to facilitate exporting tables (as shown) to a file (such as CSV).
 * <p>
 * Filters, sorting, column order, and column visibility may all impact the data exported.
 *
 * @param <T> the type of the table.
 * @since 2.7.0
 * @see TableExportButton
 */
public class TableExportAction<T extends JTable> extends AbstractAction {

	private static final long serialVersionUID = 1L;

	private static final Logger LOGGER = Logger.getLogger(TableExportAction.class);
	
	private static final String CSV_EXTENSION = ".csv";

	/**
	 * The default text for the action.
	 * <p>
	 * Lazily initialised.
	 * 
	 * @see #getDefaultText()
	 */
	private static String defaultText;

	/**
	 * The default icon for the action.
	 * <p>
	 * Lazily initialised.
	 * 
	 * @see #getDefaultIcon()
	 */
	private static Icon defaultIcon;

	private T exportTable;

	/**
	 * Constructs a {@code TableExportAction} with the given table and default text/icon.
	 *
	 * @param table the Table for which the data should be exported.
	 * @see #getDefaultText()
	 * @see #getDefaultIcon()
	 */
	public TableExportAction(T table) {
		this(table, getDefaultText(), getDefaultIcon());
	}

	/**
	 * Constructs a {@code TableExportAction} with the given table and name and with default icon.
	 *
	 * @param table the Table for which the data should be exported.
	 * @param name the name of the action.
	 * @see #getDefaultIcon()
	 */
	public TableExportAction(T table, String name) {
		this(table, name, getDefaultIcon());
	}

	/**
	 * Constructs a {@code TableExportAction} with the given table, name, and icon.
	 *
	 * @param table the Table for which the data should be exported
	 * @param name the name of the action.
	 * @param icon the icon of the action.
	 */
	public TableExportAction(T table, String name, Icon icon) {
		super(name, icon);
		exportTable = table;
	}

	/**
	 * Gets the default text used for the action.
	 *
	 * @return the default text.
	 */
	public static String getDefaultText() {
		if (defaultText == null) {
			defaultText = Constant.messages.getString("export.button.name");
		}
		return defaultText;
	}

	/**
	 * Gets the default icon used for the action.
	 *
	 * @return the default icon.
	 */
	public static Icon getDefaultIcon() {
		if (defaultIcon == null) {
			defaultIcon = new ImageIcon(TableExportAction.class.getResource("/resource/icon/16/115.png"));
		}
		return defaultIcon;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		WritableFileChooser chooser = new WritableFileChooser(Model.getSingleton().getOptionsParam().getUserDirectory()) {

			private static final long serialVersionUID = 1L;

			@Override
			public void approveSelection() {
				File file = getSelectedFile();
				if (file != null) {
					String filePath = file.getAbsolutePath();
					if (!filePath.toLowerCase(Locale.ROOT).endsWith(CSV_EXTENSION)) {
						setSelectedFile(new File(filePath + CSV_EXTENSION));
					}
				}

				super.approveSelection();
			}
		};

		chooser.setSelectedFile(new File(Constant.messages.getString("export.button.default.filename")));
		if (chooser.showSaveDialog(View.getSingleton().getMainFrame()) == WritableFileChooser.APPROVE_OPTION) {
			boolean success = true;
			try (CSVPrinter pw = new CSVPrinter(
					Files.newBufferedWriter(chooser.getSelectedFile().toPath(), StandardCharsets.UTF_8),
					CSVFormat.DEFAULT)) {
				pw.printRecord(getColumnNames());
				int rowCount = getTable().getRowCount();
				for (int row = 0; row < rowCount; row++) {
					pw.printRecord(getRowCells(row));
				}
			} catch (Exception ex) {
				success = false;
				JOptionPane.showMessageDialog(View.getSingleton().getMainFrame(),
						Constant.messages.getString("export.button.error") + "\n"
								+ ex.getMessage());
				LOGGER.error("Export Failed: " + ex.getMessage(), ex);
			}

			// Delay the presentation of success message, to ensure all the data was already flushed.
			if (success) {
				JOptionPane.showMessageDialog(View.getSingleton().getMainFrame(),
						Constant.messages.getString("export.button.success"));
			}
		}
	}
	
	/**
	 * Gets a {@code List} of (visible) column names for the given table.
	 * <p>
	 * Called when exporting the column names.
	 * 
	 * @return the {@code List} of column names, never {@code null}.
	 */
	protected List<String> getColumnNames() {
		List<String> columnNamesList = new ArrayList<>();
		for (int col = 0; col < getTable().getColumnCount(); col++) {
			columnNamesList.add(getTable().getColumnModel().getColumn(col).getHeaderValue().toString());
		}
		return columnNamesList;
	}
	
	/**
	 * Gets the cell values (in view order) for the given row.
	 * <p>
	 * Called for each (visible) row that's being exported.
	 *
	 * @param row the row, in view coordinates.
	 * @return a {@code List} containing the values of the cells for the given row, never {@code null}.
	 */
	protected List<Object> getRowCells(int row) {
		List<Object> cells = new ArrayList<>();
		for (int col = 0; col < getTable().getColumnCount(); col++) {
			Object value = getTable().getValueAt(row, col);
			cells.add(value == null ? "" : value.toString());
		}
		return cells;
	}
	
	/**
	 * Gets the Table which this button is associated with.
	 * 
	 * @return the Table this button is associated with
	 */
	protected T getTable() {
		return exportTable;
	}
	
	/**
	 * Sets the Table this button is for.
	 * 
	 * @param table the Table this button applies to
	 */
	public void setTable(T table) {
		this.exportTable = table;
	}

}
